package com.google.code.stalkernet.service;

import java.io.Closeable;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.google.code.stalkernet.database.Database;
import com.google.code.stalkernet.database.event.DatabaseEvent;
import com.google.code.stalkernet.database.event.DatabaseListener;
import com.google.code.stalkernet.entry.Entry;
import com.google.code.stalkernet.entry.EntryID;
import com.google.code.stalkernet.entry.EntryUtils;

/**
 * Service that persists {@link Database} to a file and restores it after
 * restart.
 *
 * @author Mikhail Vladimirov
 */
public class DatabasePersistanceService implements Service
{
    private final static Logger logger =
        Logger.getLogger (DatabasePersistanceService.class.getName ());

    private final MyDatabaseListener databaseListener =
        new MyDatabaseListener ();

    private final File file;
    private final Database database;

    private DataOutput dataOutput = null;

    /**
     * Create new database perststance service that persists given database to
     * given file.
     *
     * @param file {@link File} to persist database to
     * @param database {@link Database} to persist
     */
    public DatabasePersistanceService (File file, Database database)
    {
        if (file == null) throw new IllegalArgumentException ("File is null");
        if (database == null)
            throw new IllegalArgumentException ("Database is null");

        this.file = file;
        this.database = database;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void start ()
    {
        RandomAccessFile raf;

        try
        {
            raf = new RandomAccessFile (file, "rw");
        }
        catch (IOException ex)
        {
            logger.log (Level.SEVERE, "Cannot open database file: " + file, ex);
            return;
        }

        long lastGoodOffset = 0L;
        try
        {
            long length = raf.length ();
            while (lastGoodOffset < length)
            {
                int idBytesCount = raf.readInt ();
                byte [] idBytes = new byte [idBytesCount];
                raf.readFully (idBytes);
                int bodyBytesCount = raf.readInt ();
                byte [] bodyBytes = new byte [bodyBytesCount];
                raf.readFully (bodyBytes);
                int parentIDsCount = raf.readInt ();
                EntryID [] parentIDs = new EntryID [parentIDsCount];
                for (int i = 0; i < parentIDsCount; i++)
                {
                    int parentIDBytesCount = raf.readInt ();
                    byte [] parentIDBytes = new byte [parentIDBytesCount];
                    raf.readFully (parentIDBytes);
                    parentIDs [i] = EntryUtils.createEntryID (parentIDBytes);
                }
                database.addEntry (
                    EntryUtils.createEntry (
                        EntryUtils.createEntryID (idBytes),
                        EntryUtils.createEntryBody (bodyBytes),
                        parentIDs));
                lastGoodOffset = raf.getFilePointer ();
            }
        }
        catch (IOException ex)
        {
            logger.log (
                Level.WARNING,
                "Cannot restore database from file: " + file,
                ex);
        }

        try
        {
            if (raf.getFilePointer () != lastGoodOffset)
                raf.seek (lastGoodOffset);
        }
        catch (IOException ex)
        {
            logger.log (Level.SEVERE, "Cannot seek database file: " + file, ex);
            return;
        }

        dataOutput = raf;

        database.addDatabaseListener (databaseListener);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop ()
    {
        if (dataOutput != null)
        {
            database.removeDatabaseListener (databaseListener);
            try
            {
                ((Closeable)dataOutput).close ();
            }
            catch (IOException ex)
            {
                logger.log (Level.SEVERE, "Cannot close file: " + file, ex);
            }
            dataOutput = null;
        }
    }

    private class MyDatabaseListener implements DatabaseListener
    {
        @Override
        public void entryAdded (DatabaseEvent databaseEvent)
        {
            if (dataOutput != null)
            {
                try
                {
                    Entry entry = databaseEvent.getEntry ();
                    byte [] idBytes = entry.getID ().getBytes ();
                    dataOutput.writeInt (idBytes.length);
                    dataOutput.write (idBytes);
                    byte [] bodyBytes = entry.getBody ().getBytes ();
                    dataOutput.writeInt (bodyBytes.length);
                    dataOutput.write (bodyBytes);
                    EntryID [] parentIDs = entry.getParentIDs ();
                    dataOutput.writeInt (parentIDs.length);
                    for (EntryID parentID: parentIDs)
                    {
                        byte [] parentIDBytes = parentID.getBytes ();
                        dataOutput.writeInt (parentIDBytes.length);
                        dataOutput.write (parentIDBytes);
                    }
                }
                catch (IOException ex)
                {
                    logger.log (
                        Level.SEVERE,
                        "Cannot write entry to file: " + file,
                        ex);

                    stop ();
                }
            }
        }
    }
}
